package com.ec50n9.server.module.user

import com.aventrix.jnanoid.jnanoid.NanoIdUtils
import com.ec50n9.server.common.ApiException
import com.ec50n9.server.common.Md5Utils
import com.ec50n9.server.common.mayBeAlreadyExist
import com.ec50n9.server.common.mayBeNotFound
import com.ec50n9.server.module.common.AppCode
import com.ec50n9.server.module.common.AuthCode
import com.ec50n9.server.module.common.PageRendered
import com.ec50n9.server.module.common.renderTo
import com.ec50n9.server.module.resource.Resource
import com.ec50n9.server.module.resource.ResourceQueryRendered
import com.ec50n9.server.module.role.Role
import com.ec50n9.server.module.role.RoleQueryRendered
import com.ec50n9.server.module.role.RoleRepository
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.data.repository.PagingAndSortingRepository
import org.springframework.stereotype.Service
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import java.util.*

interface UserRepository : PagingAndSortingRepository<User, String> {
    fun findByUsername(username: String): Optional<User>
}

@Service
class UserService(private val userRepository: UserRepository) {

    private val name = "用户"

    fun findOne(id: String): User = mayBeNotFound(name) {
        userRepository.findById(id).orElseThrow()
    }

    fun findOneByUsername(username: String): User = mayBeNotFound(name) {
        userRepository.findByUsername(username).orElseThrow()
    }

    fun findAll(page: Int?, size: Int?, sortBy: String?) = userRepository.findAll(
        PageRequest.of(
            (page ?: 1) - 1, size ?: 10, Sort.by(sortBy ?: "id").ascending()
        )
    )

    fun authenticateUser(username: String, password: String): User {
        val userOptional = userRepository.findByUsername(username)
        if (!userOptional.isPresent) throw ApiException(AuthCode.AUTH_FAIL, "用户名或密码错误")
        val user = userOptional.get()
        var finalPassword = password
        user.salt?.let { finalPassword = Md5Utils.md5(password + it) }
        if (finalPassword != user.password) throw ApiException(AuthCode.AUTH_FAIL, "用户名或密码错误")
        return user
    }

    fun newOne(user: User) = mayBeAlreadyExist(name) {
        user.salt = NanoIdUtils.randomNanoId()
        user.password = Md5Utils.md5(user.password + user.salt)
        userRepository.save(user)
    }

    fun update(user: User) = userRepository.save(user)

    fun delete(user: User) = userRepository.delete(user)
}

@Tag(name = "用户管理")
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService, private val roleRepository: RoleRepository) {

    private fun role(code: String): Role =
        roleRepository.findByCode(code).orElseThrow { ApiException(AppCode.OBJECT_NOT_FOUND, "角色不存在") }

    @Operation(summary = "分页获取全部")
    @GetMapping
    fun findAllWithPagingAndSorting(
        page: Int?,
        size: Int?,
        sortBy: String?
    ):
            PageRendered<UserQueryRendered> =
        PageRendered(
            userService.findAll(page, size, sortBy).map { user -> user.renderTo(UserQueryRendered::class.java) }
        )


    @Operation(summary = "通过username获取")
    @GetMapping("/{username}")
    fun findOne(@PathVariable username: String): UserQueryRendered =
        userService.findOneByUsername(username).renderTo(UserQueryRendered::class.java)

    @Operation(summary = "新建")
    @PostMapping
    fun newUser(@RequestBody @Validated createForm: UserCreateForm): UserQueryRendered =
        userService
            .newOne(createForm.renderTo())
            .renderTo(UserQueryRendered::class.java)

    @Operation(summary = "通过username更新(全量)")
    @PutMapping("/{username}")
    fun replaceUser(
        @RequestBody @Validated replaceForm: UserReplaceForm,
        @PathVariable username: String
    ): UserQueryRendered {
        val user = userService.findOneByUsername(username)
        replaceForm.renderTo(user)
        return userService.update(user).renderTo(UserQueryRendered::class.java)
    }

    @Operation(summary = "通过username更新(增量)")
    @PatchMapping("/{username}")
    fun updateUser(@RequestBody updateForm: UserUpdateForm, @PathVariable username: String): UserQueryRendered {
        val user = userService.findOneByUsername(username)
        updateForm.renderTo(user)
        return userService.update(user).renderTo(UserQueryRendered::class.java)
    }

    @Operation(summary = "删除")
    @DeleteMapping("/{username}")
    fun delete(@PathVariable username: String) =
        userService.delete(userService.findOneByUsername(username))

    @Operation(summary = "角色列表")
    @GetMapping("/{username}/roles")
    fun getRoles(@PathVariable username: String): List<RoleQueryRendered> =
        userService.findOneByUsername(username).roles.renderTo(RoleQueryRendered::class.java)

    @Operation(summary = "替换角色列表")
    @PostMapping("/{username}/roles")
    fun replaceRoles(
        @RequestBody roleCodeList: List<String>,
        @PathVariable username: String
    ) {
        val user = userService.findOneByUsername(username)
        user.roles.clear()
        user.roles.addAll(roleRepository.findByCodeIn(roleCodeList))
        userService.update(user)
    }

    @Operation(summary = "绑定角色")
    @PutMapping("/{username}/roles/{roleCode}")
    fun addRole(@PathVariable username: String, @PathVariable roleCode: String) {
        val role = role(roleCode)
        val user = userService.findOneByUsername(username)
        user.roles.add(role)
        userService.update(user)
    }

    @Operation(summary = "解绑角色")
    @DeleteMapping("/{username}/roles/{roleCode}")
    fun removeRole(@PathVariable username: String, @PathVariable roleCode: String) {
        val user = userService.findOneByUsername(username)
        user.roles.removeIf { role -> role.code == roleCode }
        userService.update(user)
    }

    @Operation(summary = "资源列表")
    @GetMapping("/{username}/resources")
    fun getResources(@PathVariable username: String): List<ResourceQueryRendered> {
        val user = userService.findOneByUsername(username)
        val resources = mutableListOf<Resource>()
        user.roles.forEach { resources.addAll(it.resources) }
        return resources.renderTo(ResourceQueryRendered::class.java)
    }
}